/*
 * Copyright (C) 2010, Funcom AS, All Rights Reserved
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  @authors
 *  Svein Gunnar Larsen <sveingunnarlarsen@gmail.com>
 *  Erik Thune Lund <culex.eriklund@gmail.com>
 *  Michel Krohn-Dale <kronicz@gmail.com>
 */

package com.funcom.peeler.tool;

import com.funcom.peeler.ComponentUtils;
import com.funcom.peeler.UpdateCont;
import com.funcom.peeler.WorkBench;
import com.funcom.peeler.action.*;
import com.funcom.peeler.component.Grid;
import com.funcom.peeler.peelable.*;
import com.jmex.bui.BContainer;
import com.jmex.bui.util.Rectangle;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;

/**
 * Used for dragging components around on the workspace with the mouse as
 * well as snapping them to the grid that's created here. Arrowkeys can also
 * be used to move components around one grid size at a time or with the Alt
 * key held, one pixel at a time.
 */
public class MoveTool extends DraggingTool {

    /**
     * The grid that's shown on the workbench.
     * Added upon construction of the tool.
     * Saved as a member in case of reparenting the grid
     * to a different component
     */
    Grid grid;
    boolean newHierarchy = false;

    public MoveTool( WorkBench workbench ) {
        super( workbench );

        grid = new Grid( bui.getWorkSpace().getComponent(), bui );
        bui.getSelectionLayer().getComponent().add( grid );
    }

    public void keyPressed( final KeyEvent e ) {

        switch( e.getKeyCode() ) {

            // Fallthrough for all arrowkey presses
            case KeyEvent.VK_LEFT:
            case KeyEvent.VK_RIGHT:
            case KeyEvent.VK_DOWN:
            case KeyEvent.VK_UP: {

                final Point movement = new Point();

                int grid_size = bui.getPeelRoot().getToolBar().getGridStep();

                // Calcluates the movement of this component based on which key was pressed and
                // whether the Alt key was held down( 1 pixel movement or "nudge" )
                if ( e.getKeyCode() == KeyEvent.VK_LEFT ) movement.x -= e.isAltDown() ? 1 : grid_size;
                if ( e.getKeyCode() == KeyEvent.VK_RIGHT ) movement.x += e.isAltDown() ? 1 : grid_size;
                if ( e.getKeyCode() == KeyEvent.VK_UP ) movement.y += e.isAltDown() ? 1 : grid_size;
                if ( e.getKeyCode() == KeyEvent.VK_DOWN ) movement.y -= e.isAltDown() ? 1 : grid_size;

                bui.execute( new WorkBench.Task() {

                    public void run() {

                        // Performs the actual movement in a WorkBench.Task and clamps
                        // the dimensions to it's parent. Also pushes a change to the workbench
                        // for undoing
                        for ( PeelComponent component : bui.getSelections() ) {

                            if ( component.isLocked() ) continue;

                            Change move = new Move( component );
                            move.startChange();

                            Point new_coords = new Point( component.getComponent().getX(), component.getComponent().getY() );
                            new_coords.translate( movement.x, movement.y );

                            Rectangle new_locs = ComponentUtils.enforceParentBounds( component.getComponent(), new_coords.x, new_coords.y );
                            component.getComponent().setBounds( new_locs.x, new_locs.y, new_locs.width, new_locs.height );

                            move.endChange();
                            workbench.pushChange( move );
                        }
                    }
                });
                break;
            }

            // Fallthrough for all keys pertaining to changes in the component list
            // for a given parent
            case KeyEvent.VK_PAGE_DOWN:
            case KeyEvent.VK_PAGE_UP:
            case KeyEvent.VK_HOME:
            case KeyEvent.VK_END: {

                bui.execute( new WorkBench.Task() {

                    public void run() {


                        // Iterate through all selected components and move them in the stack according to
                        // which key was pressed, then push the change to the workbench
                        for ( PeelComponent component : bui.getSelections() ) {

                            if ( component.isLocked() ) continue;

                            BContainer parent = component.getComponent().getParent();
                            int currentIndex = parent.getComponentIndex(component.getComponent());

                            parent.remove(currentIndex);

                            switch ( e.getKeyCode() ) {

                                // Page down == move component one step down in the stack
                                case KeyEvent.VK_PAGE_DOWN: {
                                    currentIndex--;
                                    break;
                                }
                                // Page Up == move component one step up in the stack
                                case KeyEvent.VK_PAGE_UP: {
                                    currentIndex++;
                                    break;
                                }
                                // Home == move component to the top
                                case KeyEvent.VK_HOME: {
                                    currentIndex = parent.getComponentCount();
                                    break;
                                }
                                // End == move component to the bottom
                                case KeyEvent.VK_END: {
                                    currentIndex = 0;
                                    break;
                                }
                            }

                            // Clamp the movement to the actual number of components,
                            // avoid negative postions, more than the maximum and so on
                            if (currentIndex < 0) currentIndex = 0;
                            if (currentIndex > parent.getComponentCount() ) currentIndex = parent.getComponentCount();

                            // Add the component in the calculated position
                            parent.add(currentIndex, component.getComponent(), component.getComponent().getBounds());

                            workbench.pushChange( new ComponentStackChange( component, currentIndex + 1, currentIndex ) );
                        }
                    }
                });
                break;
            }
        }
    }

    /**
     * Right clicking the mouse when this tool is active will cancel
     * it and undo any changes it made.
     *
     * If the workspace is clicked, the selections are all cleared.
     * If there is only one component selected, deselect it and select
     * the new one that's been clicked on( even if it's the same one ).
     *
     * If moving is to be done, initializes the necessary changes and
     * mouse info points.
     * @param e
     */
    public void mousePressed( MouseEvent e ) {

        if ( e.getButton() == MouseEvent.BUTTON3 && change != null ) {
            deInit();
            workbench.undo();
            return;
        }

        PeelComponent component = bui.getHitComponent( e );

        if ( component == null || component instanceof PeelWorkSpace || component instanceof PeelCanvasWindow ) {
            bui.clearSelections();
            return;
        }

        if ( bui.getSelections().size() == 1 ) bui.clearSelections();

        bui.select( component );

        if ( e.getButton() == MouseEvent.BUTTON1 ) {
            prev_point = e.getPoint();
            start_point = e.getPoint();
            end_point = e.getPoint();

            Collection<Change> changes = new ArrayList<Change>();
            for ( PeelComponent tmp : bui.getSelections() ) {
                changes.add( new Resize( tmp ) );
                changes.add( new Move( tmp ) );
            }

            change = new MultiChange( changes );
            change.startChange();
            change.endChange();
        }
    }

    /**
     * When done moving, call deInit() and update the component list
     * in the property window
     * @param e the mouse event to check for which button is released
     */
    public void mouseReleased( MouseEvent e ) {

        if (newHierarchy) {
            UpdateCont.get().componentHierarchyChanged();
            newHierarchy = false;
        }

        if ( e.getButton() == MouseEvent.BUTTON1 )
            deInit();
    }

    /**
     * When the mouse is dragged, do either the snapMoving or dragMoving
     * depending on whether snapping is enabled and the amount of movement per frame
     * is slow enough. Do not start moving if the mouse points are not
     * initialized properly( the moving code will only trigger if the
     * left mouse button is clicked and held )
     * @param e the mouse event containing the coords to pass on
     */
    public void mouseDragged( final MouseEvent e ) {

        if ( prev_point == null ) return;
        updateDelta( e );

        if ( ( move_delta.distance( new Point() ) < 5 ) &&  bui.getPeelRoot().getToolBar().snapEnabled() )  {
            snapMove( e );
        }
        else {
            dragMove( e );
        }
    }

    /**
     * When moving the component around, it will snap to coordinates corresponding to the
     * grid size + the offset that center the grid on the canvas window and the direction
     * of movement. Depending on the direction of movement, the components can snap to the
     * top, bottom, left and right of grid lines
     * @param e
     */
    private void snapMove( final MouseEvent e ) {

        bui.execute( new WorkBench.Task() {

            public void run() {

                if (change == null ) return;

                change.undoChange();

                for ( PeelComponent component : bui.getSelections() ) {

                    if ( component == null ) return;
                    if ( component instanceof PeelWorkSpace ) return;
                    if ( component  instanceof PeelCanvasWindow ) return;

                    // if locked, do nothing
                    if ( component.isLocked() ) continue;

                    // get the grid step and offset for the grid
                    int grid_step = bui.getPeelRoot().getToolBar().getGridStep();
                    final Point offset = new Point( bui.getWorkSpace().getCanvas().getComponent().getX() % grid_step, bui.getWorkSpace().getCanvas().getComponent().getY() % grid_step );

                    // get the old coords and translate the mouse coords to workspace coords
                    Point old_coords = new Point( component.getComponent().getX(), component.getComponent().getY() );
                    for ( PeelComponent tmp = component.getParent() ; !(tmp instanceof PeelWorkSpace) ; tmp = tmp.getParent() ) {
                        old_coords.translate( tmp.getComponent().getX(), tmp.getComponent().getY() );
                    }

                    // Get the mouse coords and translate them to workspace coords
                    Point mouse_coords = new Point( e.getPoint() );
                    mouse_coords.translate( -bui.getWorkSpace().getComponent().getX(), -bui.getWorkSpace().getComponent().getY() );

                    // get the old coords + how much the mouse has moved - the offset of the grid for native grid coords
                    // then divide by the grid stepping size to find out which grid square it concerns( int division, gives us the integer of the grid square )
                    Point grid_loc = new Point( ( old_coords.x + start_end_delta.x - offset.x ) / grid_step ,
                                                ( old_coords.y + start_end_delta.y - offset.y ) / grid_step );

                    // multiply by the grid step to find workspace coords for the grid square to snap to
                    grid_loc.x *= grid_step;
                    grid_loc.y *= grid_step;
                    grid_loc.translate( offset.x, offset.y );

                    // snap the coords to the appropriate side of the grid square
                    directionalSnap( component, grid_loc, mouse_coords, offset );

                    // Find the new parent for this component
                    PeelContainer new_parent = findNewParent( component, e );
                    if ( new_parent == null ) return;

                    // translate the workspace coords to container local coords
                    for ( PeelContainer tmp = new_parent ; !(tmp instanceof PeelWorkSpace) ; tmp = tmp.getParent() ) {
                        grid_loc.translate( -tmp.getComponent().getX(), -tmp.getComponent().getY() );
                    }

                    // reparent the component if the parent is not the same as the existing one
                    if ( new_parent != component.getParent() ) {
                        reparent( component, new_parent );
                        newHierarchy = true;
                    }

                    // clamp the position to the parent container and do the actual moving
                    com.jmex.bui.util.Rectangle new_locs = ComponentUtils.enforceParentBounds( component.getComponent(), grid_loc.x, grid_loc.y );
                    component.getComponent().setBounds( new_locs.x, new_locs.y, new_locs.width, new_locs.height );

                    change.endChange();
                }
            }

            private void directionalSnap( PeelComponent component, Point grid_loc, Point mouse_coords, Point offset ) {

                int grid_size = bui.getPeelRoot().getToolBar().getGridStep();

                if ( move_delta.x > 0 ) {
                    grid_loc.x += ( grid_size - ( component.getComponent().getWidth() % grid_size ) );
                }

                if ( move_delta.y > 0 ) {
                    grid_loc.y += ( grid_size - ( component.getComponent().getHeight() % grid_size ) );
                }
            }
        });
    }

    private void dragMove( final MouseEvent e ) {

        bui.execute( new WorkBench.Task() {

            public void run() {

                if (change == null ) return;

                change.undoChange();

                for( PeelComponent component : bui.getSelections() ) {

                    if ( component == null ) return;
                    if ( component instanceof PeelWorkSpace) return;
                    if ( component instanceof PeelCanvasWindow) return;

                    if ( component.isLocked() ) continue;

                    Point old_coords = new Point( component.getComponent().getX(), component.getComponent().getY() );
                    Point new_coords = new Point( old_coords.x + start_end_delta.x, old_coords.y + start_end_delta.y );

                    // Transform the new coords to workspace coords
                    for ( PeelComponent tmp = component.getParent() ; !(tmp instanceof PeelWorkSpace) ; tmp = tmp.getParent() ) {
                        new_coords.translate( tmp.getComponent().getX(), tmp.getComponent().getY() );
                    }

                    // Find the new parent for this component
                    PeelContainer new_parent = findNewParent( component, e );
                    if ( new_parent == null ) return;

                    // If we have a new parent, transfer this component to it and translate coordinates
                    if( new_parent != component.getParent() ) {
                        reparent( component, new_parent );
                        newHierarchy = true;
                    }

                    // translate the workspace coords to the new parent local coords
                    for ( PeelComponent tmp = new_parent ; !(tmp instanceof PeelWorkSpace) ; tmp = tmp.getParent() ) {
                        new_coords.x -= tmp.getComponent().getX();
                        new_coords.y -= tmp.getComponent().getY();
                    }

                    // clamp the position to the parent container and do the actual moving
                    com.jmex.bui.util.Rectangle new_locs = ComponentUtils.enforceParentBounds( component.getComponent(), new_coords.x, new_coords.y );
                    component.getComponent().setBounds( new_locs.x, new_locs.y, new_locs.width, new_locs.height );

                    change.endChange();
                }
            }
        });
    }

    protected PeelContainer findNewParent( PeelComponent component, MouseEvent e ) {

        int big_number = 3000000;
        // Move the component away and check what's beneath the cursor, then move the component back again
        Point old_coords = new Point( component.getComponent().getX(), component.getComponent().getY() );
        component.getComponent().setLocation( big_number, big_number );
        PeelComponent new_parent = bui.getHitComponent( e );
        component.getComponent().setLocation( old_coords.x, old_coords.y );

        if ( new_parent == null ) return null;

        // We don't want the new parent to be either a non-container or a placeholder, find the first PeelContainer that's good
        while( !(new_parent instanceof PeelContainer) || new_parent instanceof PeelPlaceHolder) {
            if ( new_parent instanceof PeelWorkSpace )break; // No suitable PeelParent all the way up, this will default to a PeelWorkSpace
            new_parent = new_parent.getParent();
        }

        return (PeelContainer)new_parent;
    }

    protected void reparent( PeelComponent component, PeelContainer container ) {

        if ( container == null ) return;
        component.detach();
        container.addChild( component );
    }
}
